package com.vision.core.sv.serv.rpc;

import java.io.Serializable;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.orm.hibernate3.HibernateTemplate;

import com.vision.core.cm.data.ActivityType;
import com.vision.core.cm.data.Domain;
import com.vision.core.cm.data.IHasSystemTime;
import com.vision.core.cm.db.data.ActivityLog;
import com.vision.core.cm.exception.DataNotFoundException;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.cm.serv.rpc.AbstractService;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.serv.BaseRpcServlet;
import com.vision.core.sv.util.BeanSerializer;

/**
 * Base class for services.
 *  
 * @author Mark
 *
 */
public abstract class AbstractServiceImpl<T> extends BaseRpcServlet implements AbstractService<T> {
	
	private static final long serialVersionUID = 362139446451591874L;
	
	private static final Logger log = LoggerFactory.getLogger(AbstractServiceImpl.class);
	
	private IDataManager<T> dataManager;
	
	@Autowired
	@Qualifier("activityLogManager")
	private IDataManager<ActivityLog<T>> activityLogManager;
	
	protected IDataManager<ActivityLog<T>> getActivityLogManager() {
		return activityLogManager;
	}

	protected IDataManager<T> getDataManager() {
		return dataManager;
	}
	
	public void setDataManager(IDataManager<T> dataManager) {
		this.dataManager = dataManager;
	}
	
	public HibernateTemplate getTemplate() {
		return dataManager.getTemplate();
	}
	
	// Standard Service Methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	public T create(T object) throws RpcException {
		if (object instanceof IHasSystemTime) {
			setSysCreatedAndLastModified((IHasSystemTime) object);
		}
		
		T result = getDataManager().create(object);
		return result;
	}
	
	public List<T> getAll() throws RpcException {
		return getDataManager().getAll();
	}
	
	public T getById(long id) throws RpcException {
		return getDataManager().getById(id);
	}
	
	public T update(T object) throws RpcException {
		if (object instanceof IHasSystemTime) {
			setSysLastModified((IHasSystemTime) object);
		}
		
		T result = getDataManager().update(object);
		return result;
	}
	
	public T delete(T object) throws RpcException {
		try {
			T result = getDataManager().delete(object);
			return result;
		} catch (Exception e) {
			if (e instanceof SQLException) {
				SQLException sqle = (SQLException) e;
				log.error(sqle.getMessage());
				// TODO Determine the cause why the SQL operation failed and report to user.
			}
			throw new RpcException("The server was unable to perform the delete operation.");
		}
	}

	/**
	 * Returns the {@link Domain} associated to this service.
	 * 
	 * @return
	 */
	protected abstract Domain getDomain();
	
	// Activity Logging Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	/**
	 * Logs a "create" operation into the database.
	 * 
	 * @param object
	 */
	protected void logCreate(T object) {
		String serialized = BeanSerializer.serialize(object);
		
		log.debug("Logging CREATE operation: {}", serialized);
			
		ActivityLog<T> aLog = new ActivityLog<T>(getLoggedInUser(), now(), getDomain(), ActivityType.CREATE, serialized);
		activityLogManager.create(aLog);
	}
	
	/**
	 * Logs an "update" operation into the database.
	 * 
	 * @param oldObject
	 * @param newObject
	 */
	protected void logUpdate(T oldObject, T newObject) {
		String serialized = BeanSerializer.serialize(newObject);
		
		log.debug("Logging UPDATE operation: {}", serialized);
		
		ActivityLog<T> aLog = new ActivityLog<T>(getLoggedInUser(), now(), getDomain(), ActivityType.UPDATE, serialized);
		activityLogManager.create(aLog);
	}
	
	/**
	 * Logs a "delete" operation into the database.
	 * 
	 * @param object
	 */
	protected void logDelete(T object) {
		String serialized = BeanSerializer.serialize(object);
		
		log.debug("Logging DELETE operation: {}", serialized);
		
		ActivityLog<T> aLog = new ActivityLog<T>(getLoggedInUser(), now(), getDomain(), ActivityType.DELETE, serialized);
		activityLogManager.create(aLog);
	}
	
	/**
	 * Logs an "associate" operation into the database.
	 * 
	 * @param object
	 */
	protected <O> void logAssociate(T object, List<O> associatedObjects) {
		String serialized = BeanSerializer.serialize(object);
		
		log.debug("Logging ASSOCIATE operation: {}", serialized);
		
		ActivityLog<T> aLog = new ActivityLog<T>(getLoggedInUser(), now(), getDomain(), ActivityType.ASSOCIATE, serialized);
		activityLogManager.create(aLog);
	}
	
	/**
	 * Logs a "deassociate" operation into the database.
	 * 
	 * @param object
	 */
	protected <O> void logDeassociate(T object, List<O> deassociatedObjects) {
		String serialized = BeanSerializer.serialize(object);
		
		log.debug("Logging DEASSOCIATE operation: {}", serialized);
		
		ActivityLog<T> aLog = new ActivityLog<T>(getLoggedInUser(), now(), getDomain(), ActivityType.DEASSOCIATE, serialized);
		activityLogManager.create(aLog);
	}
	
	// IHasSystemTime Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	protected void setSysCreatedAndLastModified(IHasSystemTime obj) {
		obj.setSysCreated(now());
		obj.setSysLastModified(obj.getSysCreated());
	}
	
	protected void setSysLastModified(IHasSystemTime obj) {
		obj.setSysLastModified(now());
	}
	
	// Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@SuppressWarnings("unchecked")
	protected <M> List<M> findByCriteria(ISimpleCriteria crit) {
		return getTemplate().findByCriteria(crit.getDetached());
	}
	
	/**
	 * Checks if there is an object in the database with id equal to the specified
	 * <tt>id</tt>. If a match is found, an {@link RpcException} is thrown.
	 * 
	 * @param id
	 * @return
	 * @throws RpcException
	 */
	protected T checkExisting(Serializable id) throws RpcException {
		T object = getDataManager().getById(id);
		if (object == null) {
			String clazz = getDataManager().getManagedClass().getSimpleName();
			throw new DataNotFoundException("The " + clazz + " with id=" + id + " does not exist.");
		}
		return object;
	}
	
	/**
	 * Checks if there is an existing row in the database with <tt>field</tt>
	 * equal to <tt>value</tt>. If a match is found, an {@link RpcException} is thrown.
	 * 
	 * @param field
	 * @param value
	 * @param fieldName - the field's name, used for reporting to the user
	 * @throws RpcException
	 */
	@SuppressWarnings("unchecked")
	protected void checkFieldUnique(String field, Object value, String fieldName) throws RpcException {
		ISimpleCriteria crit = SimpleCriteria.forClass(getDataManager().getManagedClass());
		if (value instanceof String) {
			crit.like(field, (String) value);
		} else {
			crit.eq(field, value);
		}
		List<T> list = getTemplate().findByCriteria(crit.getDetached());
		if (!list.isEmpty())
			throw new RpcException(fieldName + " " + value + " is already in use.");
	}
	
	public static Timestamp now() {
		return new Timestamp(System.currentTimeMillis());
	}
	
}
